package chair;

import java.util.ArrayList;

import chair.core.CoreModel;
import chair.core.Stimulus;

/**
 * Va connecter les différents organes, les réflexes, contenir le modèle.
 * 
 * Il faudra faire tourner tick() dans le main du programme pour tout faire
 * fonctionner.
 * 
 * Lancera les thread Striatum (déroulement réflexes/comportements) et
 * Thalamus(écoute organes et prédictions associées) lors du premier appel à
 * tick().
 * 
 * @author nomhad
 * 
 */
final public class Brain {
	/**
	 * Plaque tournante des informations sensorielles
	 * 
	 * FIXME: liste doublon avec celle contenue dans Thalamus (transition vers
	 * multi-thread)
	 */
	private ArrayList<Organ> _listOrgans;
	/** OK, peut-être pas le meilleur nom. Quand accumbens pour récompense */
	private CoreModel _amygdale;
	/** Gère l'indice de satisfaction du robot */
	private Hippocampus _hippo;
	/** La liste des fonctions de l'organisme */
	private ArrayList<Function> _listFunctions;
	/** Un thread écoute les organes */
	private Thalamus _thalamus;
	/** Un autre thread fait tourner les fonctions */
	private Striatum _striatum;
	/**
	 * Le cerveau devra savoir que c'est son premier tick() pour pouvoir lancer
	 * les threads Striatum et Thalamus via init()
	 */
	private boolean _initialized = false;

	public Brain(CoreModel model) {
		_amygdale = model;
		// Initialise les listes des composants
		_listOrgans = new ArrayList<Organ>();
		_listFunctions = new ArrayList<Function>();
		// Les deux autres moteurs de la simulation
		_thalamus = new Thalamus(this);
		_striatum = new Striatum();
		// Hippocampe à créer et à relier au thalamus
		_hippo = new Hippocampus();
	}

	/**
	 * Va connecter les organes aux réflexes qu'ils entraînent.
	 * 
	 * Attention, l'ordre de branchement est important : les premiers réflexes
	 * ajoutés seront les premiers réflexes exécutés (à prendre en considération
	 * s'ils utilisent la même fonction).
	 * 
	 * Attention, ici ce sont toutes les valeurs renvoyées par l'organe (sauf 0)
	 * qui entraîneront le réflexe, jongler avec plug(Organ org, byte value,
	 * Reflex res) pour un ordre précis suivant sensation.
	 * 
	 * 0 étant par définition une absence de donnée, il faut lui brancher
	 * explicitement un réflexe via la méthode plug(Organ, byte, Reflex)
	 * 
	 * @param org
	 *            organe ayant le bonheur d'entraîner un réflexe
	 * @param res
	 *            réflexe en question
	 */
	public void plug(Organ org, Reflex res) {
		if (!_listOrgans.contains(org))
			throw new IllegalArgumentException("Organe non relié à ce cerveau");
		org.plugToReflex(res);
	}

	/**
	 * Un organe peut enregistrer des sensations à la volée. Cette méthode sert
	 * à définir un réflexe par défaut à leur associer. Dès l'appel de cette
	 * méthode, tous les senseurs que l'organe crée seront reliés au réflexe.
	 * Pour relier ceux déjà existant, utiliser les plug() classiques.
	 * 
	 * @param org
	 *            organe ayant le bonheur potentiel d'entraîner un réflexe
	 * @param res
	 *            réflexe en question
	 */
	public void plugDefault(Organ org, Reflex res) {
		if (!_listOrgans.contains(org))
			throw new IllegalArgumentException("Organe non relié à ce cerveau");
		org.plugToDefaultReflex(res);
	}

	/**
	 * Similaire à plug(Organ org, Reflex res), excepté qu'on choisi
	 * spécifiquement une valeur retournée par l'organe.
	 * 
	 * @param org
	 *            organe ayant le bonheur d'entraîner un réflexe
	 * @param value
	 *            à quelle valeur exactement on veut avoir affaire
	 * @param res
	 *            réflexe en question
	 */
	public void plug(Organ org, byte value, Reflex res) {
		if (!_listOrgans.contains(org))
			throw new IllegalArgumentException("Organe non relié à ce cerveau");
		org.plugToReflex(res, value);
	}

	/**
	 * Utilisé si un organe doit utiliser certaines fonction du robot pour
	 * remplir son contrat. On laisse Organ vérifier qu'on ne lui ajoute pas
	 * deux fois la même fonction.
	 * 
	 * @param org
	 *            organe sur le point d'agir sur son petit monde
	 * @param fct
	 *            un des petits mondes en question
	 */
	public void plug(Organ org, Function fct) {
		org.addFunction(fct);
	}

	/**
	 * Un organe s'enregistre lors de sa construction auprès de Brain. Ici on en
	 * profite pour l'ajouter à Thalamus. Son stimulus sera ajouté au modèle
	 * quand quand Organ appelera plugStimulus.
	 * 
	 * @param org
	 *            l'organe qui veut rentrer dans la bande
	 */
	void addOrgan(Organ org) {
		if (_listOrgans.contains(org))
			throw new IllegalArgumentException("Organe déjà relié à ce cerveau");
		_listOrgans.add(org);
		// On lance le bébé à Thalamus pour monitorer changements
		_thalamus.add(org);
	}

	/**
	 * Un Organ a besoin de pouvoir communiquer avec le Striatum afin de lui
	 * signaler la production de réflexes/réponses
	 * 
	 * @return référence vers le striatum de cet organisme
	 */
	Striatum getStriatum() {
		return _striatum;
	}

	/**
	 * Appelé par Thalamus et Visceral lors de construction afin de savoir où se
	 * trouve le gérant du contexte
	 * 
	 * @return _hippo-potamus : de la bonne viande au menu !
	 */
	Hippocampus getHippocampus() {
		return _hippo;
	}

	/**
	 * Ajoute un stimulus au modèle. Appelé par Organ si celui-ci en définit un
	 * ou lors de addBehavior pour la même raison.
	 * 
	 * @param stim
	 *            nouveau stimulus à considérer
	 */
	void plugStimulus(Stimulus stim) {
		_amygdale.plugStimulus(stim);
	}

	/**
	 * Ajoute un comportement au robot. Ira chercher Function correspondante
	 * pour l'y enregistrer. En profite pour ajouter au modèle le stimulus
	 * associé au comportement s'il existe.
	 * 
	 * @param bev
	 *            Behavior à ajouter au robot
	 */
	public void addBehavior(Behavior bev) {
		bev.getFunction().addBehavior(bev);
		Stimulus stim = bev.getStimulus();
		if (stim != null) {
			plugStimulus(stim);
		}
	}

	/**
	 * Une fonction demande à s'enregistrer auprès de Brain
	 * 
	 * @param fct
	 *            Fonction à brancher
	 */
	void addFunction(Function fct) {
		if (_listFunctions.contains(fct))
			throw new IllegalArgumentException(
					"Fonction déjà reliée à ce cerveau");
		_listFunctions.add(fct);
		// Striatum a la garde des enfants ce WE. Et tous les jours qui suivent.
		_striatum.add(fct);
	}

	/**
	 * L'organisme est sensé être constitué lors du premier tick() : on lance
	 * les thread à l'écoute de l'environnement
	 */
	private void init() {
		_thalamus.init();
		_striatum.init();
	}

	/**
	 * Va faire agir chaque organe puis mettre à jour le modèle. Lors du premier
	 * appel va lancer Thalamus et Striatum. L'écoulement du temps, destiné au
	 * modèle du conditionnement, est géré en interne.
	 * 
	 * FIXME: permettre de lire temps du cerveau pour debug dans main
	 * 
	 */
	public void tick() {
		// Premier appel déclenche initialisation implicite
		// TODO: s'économiser un test par cycle en le faisant faire
		// explicitement par l'utilisateur ?
		if (!_initialized) {
			init();
			_initialized = true;
		}

		synchronized (_listOrgans) {
			for (Organ org : _listOrgans) {
				org.tick();
			}
		}
		_amygdale.tick(Chronos.top());
	}

	/**
	 * Pour passer ici faut payer ! Interface avec le modèle de conditionnement
	 * et l'hippocampe
	 * 
	 * @param stim
	 *            stimulus associé à l'évènement
	 * @param risingEdge
	 *            front montant ou descendant
	 */
	void handleEvent(Stimulus stim, boolean risingEdge) {
		_amygdale.handleEvent(stim, risingEdge);
		// On ne dérange le cheval que pour un front montant
		if (risingEdge)
			_hippo.handleStimulus(stim);
	}

	/**
	 * Interface entre Function et CoreModel
	 * 
	 * @param stim
	 *            stimulus dont on veut connaîtres les éventuels conséquences
	 * @param risingEdge
	 *            front montant (true) ou descendan
	 * @return liste des stimuli prévus
	 */
	ArrayList<Stimulus> checkForecastProbability(Stimulus stim,
			boolean risingEdge) {
		return _amygdale.checkForecastProbability(stim, risingEdge);
	}

	/**
	 * Interface entre Function et CoreModel
	 * 
	 * @param stim
	 *            stimulus dont on veut savoir s'il va bientôt survenir
	 * @return true si devrait survenir, false sinon
	 */
	boolean checkForecast(Stimulus stim) {
		return _amygdale.checkForecast(stim);
	}

	/**
	 * Retourne des informations concernant le modèle
	 */
	public String debug() {
		return _amygdale.debug();
	}
}
